package com.bayleaf.common.encoding;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;


public class JHConverterImpl implements JHConverter {
	private static final int	UNICODE_HANGUL_GA	= 0xAC00;
	private static final int	UNICODE_HANGUL_HI	= 0xD7A3;
	private static final char	HANGUL_FILL			= 0x3164;
	private static final char	COMPATIBLE_JAUM_G	= 0x3131;
	private static final char	COMPATIBLE_JAUM_H	= 0x314E;
	private static final char	COMPATIBLE_MOUM_A	= 0x314F;
	private static final char	COMPATIBLE_MOUM_E	= 0x3163;
	private static final char	UNICODE_CHO_G		= 0x1100;
	private static final char	UNICODE_CHO_H		= 0x1112;
	private static final char	UNICODE_JUNG_A		= 0x1161;
	private static final char	UNICODE_JUNG_E		= 0x1175;
	private static final char	UNICODE_JONG_G		= 0x11A8;
	private static final char	UNICODE_JONG_H		= 0x11C2;
	
	private static final Map<Integer, Integer>	UniToMS949Cho	= new HashMap<Integer, Integer>();
	private static final Map<Integer, Integer>	UniToMS949Jong	= new HashMap<Integer, Integer>();
	private static final Map<Integer, Integer>	MS949ToUniCho	= new HashMap<Integer, Integer>();
	private static final Map<Integer, Integer>	MS949ToUniJong	= new HashMap<Integer, Integer>();
	
	static {
		// 초성
		UniToMS949Cho.put(0, 0);		// ㄱ
		UniToMS949Cho.put(1, 1);		// ㄲ
		UniToMS949Cho.put(2, 3);		// ㄴ
		UniToMS949Cho.put(3, 6);		// ㄷ
		UniToMS949Cho.put(4, 7);		// ㄸ
		UniToMS949Cho.put(5, 8);		// ㄹ
		UniToMS949Cho.put(6, 16);		// ㅁ
		UniToMS949Cho.put(7, 17);		// ㅂ
		UniToMS949Cho.put(8, 18);		// ㅃ
		UniToMS949Cho.put(9, 20);		// ㅅ
		UniToMS949Cho.put(10, 21);		// ㅆ
		UniToMS949Cho.put(11, 22);		// ㅇ
		UniToMS949Cho.put(12, 23);		// ㅈ
		UniToMS949Cho.put(13, 24);		// ㅉ
		UniToMS949Cho.put(14, 25);		// ㅊ
		UniToMS949Cho.put(15, 26);		// ㅋ
		UniToMS949Cho.put(16, 27);		// ㅌ
		UniToMS949Cho.put(17, 28);		// ㅍ
		UniToMS949Cho.put(18, 29);		// ㅎ
		
		// 종성
		UniToMS949Jong.put(1, 0);		// ㄱ
		UniToMS949Jong.put(2, 1);		// ㄲ
		UniToMS949Jong.put(3, 2);		// ㄳ
		UniToMS949Jong.put(4, 3);		// ㄴ
		UniToMS949Jong.put(5, 4);		// ㄵ
		UniToMS949Jong.put(6, 5);		// ㄶ
		UniToMS949Jong.put(7, 6);		// ㄷ
		UniToMS949Jong.put(8, 8);		// ㄹ
		UniToMS949Jong.put(9, 9);		// ㄺ
		UniToMS949Jong.put(10, 10);		// ㄻ
		UniToMS949Jong.put(11, 11);		// ㄼ
		UniToMS949Jong.put(12, 12);		// ㄽ
		UniToMS949Jong.put(13, 13);		// ㄾ
		UniToMS949Jong.put(14, 14);		// ㄿ
		UniToMS949Jong.put(15, 15);		// ㅀ
		UniToMS949Jong.put(16, 16);		// ㅁ
		UniToMS949Jong.put(17, 17);		// ㅂ
		UniToMS949Jong.put(18, 19);		// ㅄ
		UniToMS949Jong.put(19, 20);		// ㅅ
		UniToMS949Jong.put(20, 21);		// ㅆ
		UniToMS949Jong.put(21, 22);		// ㅇ
		UniToMS949Jong.put(22, 23);		// ㅈ
		UniToMS949Jong.put(23, 25);		// ㅊ
		UniToMS949Jong.put(24, 26);		// ㅋ
		UniToMS949Jong.put(25, 27);		// ㅌ
		UniToMS949Jong.put(26, 28);		// ㅍ
		UniToMS949Jong.put(27, 29);		// ㅎ
		
		// 초성
		MS949ToUniCho.put(0x3131, 0);		// ㄱ
		MS949ToUniCho.put(0x3132, 1);		// ㄲ
		MS949ToUniCho.put(0x3134, 2);		// ㄴ
		MS949ToUniCho.put(0x3137, 3);		// ㄷ
		MS949ToUniCho.put(0x3138, 4);		// ㄸ
		MS949ToUniCho.put(0x3139, 5);		// ㄹ
		MS949ToUniCho.put(0x3141, 6);		// ㅁ
		MS949ToUniCho.put(0x3142, 7);		// ㅂ
		MS949ToUniCho.put(0x3143, 8);		// ㅃ
		MS949ToUniCho.put(0x3145, 9);		// ㅅ
		MS949ToUniCho.put(0x3146, 10);		// ㅆ
		MS949ToUniCho.put(0x3147, 11);		// ㅇ
		MS949ToUniCho.put(0x3148, 12);		// ㅈ
		MS949ToUniCho.put(0x3149, 13);		// ㅉ
		MS949ToUniCho.put(0x314A, 14);		// ㅊ
		MS949ToUniCho.put(0x314B, 15);		// ㅋ
		MS949ToUniCho.put(0x314C, 16);		// ㅌ
		MS949ToUniCho.put(0x314D, 17);		// ㅍ
		MS949ToUniCho.put(0x314E, 18);		// ㅎ
		
		// 종성
		MS949ToUniJong.put(0x3131, 1);		// ㄱ
		MS949ToUniJong.put(0x3132, 2);		// ㄲ
		MS949ToUniJong.put(0x3133, 3);		// ㄳ
		MS949ToUniJong.put(0x3134, 4);		// ㄴ
		MS949ToUniJong.put(0x3135, 5);		// ㄵ
		MS949ToUniJong.put(0x3136, 6);		// ㄶ
		MS949ToUniJong.put(0x3137, 7);		// ㄷ
		MS949ToUniJong.put(0x3139, 8);		// ㄹ
		MS949ToUniJong.put(0x313A, 9);		// ㄺ
		MS949ToUniJong.put(0x313B, 10);		// ㄻ
		MS949ToUniJong.put(0x313C, 11);		// ㄼ
		MS949ToUniJong.put(0x313D, 12);		// ㄽ
		MS949ToUniJong.put(0x313E, 13);		// ㄾ
		MS949ToUniJong.put(0x313F, 14);		// ㄿ
		MS949ToUniJong.put(0x3140, 15);		// ㅀ
		MS949ToUniJong.put(0x3141, 16);		// ㅁ
		MS949ToUniJong.put(0x3142, 17);		// ㅂ
		MS949ToUniJong.put(0x3144, 18);		// ㅄ
		MS949ToUniJong.put(0x3145, 19);		// ㅅ
		MS949ToUniJong.put(0x3146, 20);		// ㅆ
		MS949ToUniJong.put(0x3147, 21);		// ㅇ
		MS949ToUniJong.put(0x3148, 22);		// ㅈ
		MS949ToUniJong.put(0x314A, 23);		// ㅊ
		MS949ToUniJong.put(0x314B, 24);		// ㅋ
		MS949ToUniJong.put(0x314C, 25);		// ㅌ
		MS949ToUniJong.put(0x314D, 26);		// ㅍ
		MS949ToUniJong.put(0x314E, 27);		// ㅎ
	}
	
	public JHConverterImpl() {}

	/**
	 * 실제적으로 MS949/EUC-KR 이외에는 깨진 한글 검출이 안됨.
	 * 다른 문자 집합이 인자로 오더라도 깨진 한글 검출이 가능한 알고리즘 개발이 필요.
	 * - US-ASCII와 ISO-8859-1 문자 집합도 검출 가능.
	 * - US-ASCII와 ISO-8859-1 은 한글 자체가 표현 불가능하기 때문에 생략.
	 */
	@Override
	public boolean detectCharOutOfRange(String unicodeStr, CharsetType charType) {
		if (unicodeStr == null || unicodeStr.length() == 0) return false;
		if (charType == null) return false;
		
		String tStr = changeCharset(unicodeStr, charType);
		char[] tChars= tStr.toCharArray();
		char[] uChars = unicodeStr.toCharArray();

		for (int i=0; i<tChars.length; ++i) {
			if (isBrokenChar(tChars[i], uChars[i])) return true;
		}
		
		return false;
	}
	
	private static boolean isBrokenChar(char tChar, char uChar) {
		int tCode = (int) tChar;
		int uCode = (int) uChar;
		if(tCode == 63 && tCode != uCode) return true;
		else return false;
	}
	
	@Override
	public String normalUnicodeToEuckrUnicode(String unicodeStr, CharsetType charType) {
		if (unicodeStr == null || unicodeStr.length() == 0) return unicodeStr;
		if (charType == null) return unicodeStr;
		
		String tStr = changeCharset(unicodeStr, charType);
		char[] tChars= tStr.toCharArray();
		char[] uChars = unicodeStr.toCharArray();
		
		StringBuilder sb = new StringBuilder();
		
		for (int i=0; i<tChars.length; ++i) {
			int uniDecCode = (int) uChars[i];
			if (isBrokenChar(tChars[i], uChars[i]) && (uniDecCode >= UNICODE_HANGUL_GA && uniDecCode <= UNICODE_HANGUL_HI)) {
				char[] chars = toCompatibleJamo(uChars[i]);
				for (char c : chars) {
					sb.append(c);
				}
			} else {
				sb.append(String.valueOf(tChars[i]));
			}
		}
		
		return sb.toString();
	}

	@Override
	public String euckrUnicodeToNormalUnicode(String unicodeStr) {
		if (unicodeStr == null || unicodeStr.length() == 0) return unicodeStr;
		StringBuilder sb = new StringBuilder();
		
		char[] uChars = unicodeStr.toCharArray();
		for (int i=0; i<uChars.length; ++i) {
			char c = uChars[i];
			if (c == HANGUL_FILL) {
				char cho = uChars[i+1];
				char jung = uChars[i+2];
				char jong = HANGUL_FILL;
				if (uChars[i+3] == HANGUL_FILL) {
					i += 3;
				} else if (isJongsung(uChars[i+3])) {
					jong = uChars[i+3];
					i += 3;
				} else {
					i += 2;
				}
				char uc = toUnicodeHangul(new char[] {cho, jung, jong});
				sb.append(uc);
			} else {
				sb.append(c);
			}
		}
		return sb.toString();
	}
	
	/**
	 * 구현은 EgovCharSetUtil 소스를 가져왔음.
	 * 문제 있으면 다시 구현해야 함.
	 */
	@Override
	public String nonNCRUnicodeToNCRUnicode(String unicodeStr, CharsetType charType) {
		if (unicodeStr == null || unicodeStr.length() == 0) return unicodeStr;
		if (charType == null) return unicodeStr;

		StringBuilder ncr = new StringBuilder();
		String euckrStr = changeCharset(unicodeStr, charType);
		
		char[] krCa= euckrStr.toCharArray();
		char[] uniCa = unicodeStr.toCharArray();
		for (int i=0; i<uniCa.length; ++i) {
			int uniDecCode = (int) uniCa[i];
			// 깨진 문자(?)이고 원래 문자가 물음표(?)가 아니면 HTML NCR로 변환한다.
			if ((uniDecCode < UNICODE_HANGUL_GA || uniDecCode > UNICODE_HANGUL_HI) && isBrokenChar(krCa[i], uniCa[i])) {
				ncr.append(toNCR(uniDecCode));
			} else {
				ncr.append(String.valueOf(uniCa[i]));
			}
		}
		return ncr.toString();
	}
	
	private static String toNCR(int charCode) {
		StringBuilder sb = new StringBuilder();
		sb.append("&#");
		sb.append(charCode);
		sb.append(";");
		return sb.toString();
	}
	
	@Override
	public String ncrUnicodeToNonNCRUnicode(String unicodeStr) {
		if (unicodeStr == null || unicodeStr.length() == 0) return unicodeStr;
		
		StringBuilder result = new StringBuilder();
		char[] uniChar = unicodeStr.toCharArray();
		int length = uniChar.length;
		
		for (int i=0; i<length; ++i) {
			if (uniChar[i] == '&' && uniChar[i+1] == '#') {
				int j = i + 2;
				StringBuilder numBuf = new StringBuilder();
				while (j<length) {
					char c = uniChar[j];
					if (Character.isDigit(c)) {
						numBuf.append(c);
						j++;
					} else if (c == ';') {
						Character co = toChar(numBuf);
						if (co != null) result.append(co.charValue());
						j++;
						i = j - 1;
						break;
					} else {
						result.append('&');
						break;
					}
				}
			} else {
				result.append(uniChar[i]);
			}
		}
		
		return result.toString();
	}

	public byte[] toEuckrKSByteArray(String unicodeStr, CharsetType targetType) {
		String r = normalUnicodeToEuckrUnicode(unicodeStr, targetType);
		try {
			return r.getBytes(targetType.toName());
		} catch (UnsupportedEncodingException e) {
			throw new JHException(e);
		}
	}
	
	private static Character toChar(StringBuilder sb) {
		if (sb.length() > 0) {
			int hex = Integer.parseInt(sb.toString());
			return new Character((char) hex);
		} else {
			return null;
		}
	}
	
	private static String changeCharset(String str, CharsetType targetType) {
		String cName = targetType.toName();
		try {
			byte[] arr = str.getBytes(cName);
			return new String(arr, cName);
		} catch (UnsupportedEncodingException e) {
			return str;
		}
	}
	
	private static char[] toCompatibleJamo(char uniChar) {
		int uValue = ((int) uniChar) - UNICODE_HANGUL_GA;
		int jong = uValue % 28;
		int jung = ((uValue - jong) / 28) % 21;
		int cho = ((uValue - jong) / 28) / 21;
		
		if (jong < 0 || jong > 27 || jung < 0 || jung > 20 || cho < 0 || cho > 18)
			throw new IndexOutOfBoundsException();
		
		char ms949Cho = (char) (COMPATIBLE_JAUM_G + UniToMS949Cho.get(cho));
		char ms949Jung = (char) (COMPATIBLE_MOUM_A + jung);
		char ms949Jong = HANGUL_FILL;
		
		if (jong > 0) {
			ms949Jong = (char) (COMPATIBLE_JAUM_G + UniToMS949Jong.get(jong));
		}
		
		return new char[] {HANGUL_FILL, ms949Cho, ms949Jung, ms949Jong};
	}
	
	private static char toUnicodeHangul(char[] compatibleJM) {
		int choIndex = getUnicodeChosungIndex(compatibleJM[0]);
		int jungIndex = getUnicodeJungsungIndex(compatibleJM[1]);
		int jongIndex = 0;
		if (compatibleJM.length >= 3) {
			jongIndex = getUnicodeJongsungIndex(compatibleJM[2]);
		}
		
		if (choIndex < 0 || jungIndex < 0 || jongIndex < 0)
			throw new JHException("Out of jamo index.");
		
		int result = UNICODE_HANGUL_GA + (choIndex * 21 * 28) + (jungIndex * 28) + jongIndex;
		return (char) result;
	}
	
	private static int getUnicodeChosungIndex(char c) {
		if (c >= COMPATIBLE_JAUM_G && c <= COMPATIBLE_JAUM_H) return MS949ToUniCho.get((int) c);
		else if (c >= UNICODE_CHO_G && c <= UNICODE_CHO_H) return (int) c - UNICODE_CHO_G;
		else return -1;
	}
	
	private static int getUnicodeJungsungIndex(char c) {
		if (c >= COMPATIBLE_MOUM_A && c <= COMPATIBLE_MOUM_E) return (int) c - COMPATIBLE_MOUM_A;
		else if (c >= UNICODE_JUNG_A && c <= UNICODE_JUNG_E) return (int) c - UNICODE_JUNG_A;
		else return -1;
	}
	
	private static int getUnicodeJongsungIndex(char c) {
		if (c == HANGUL_FILL) return 0;
		else if (c >= COMPATIBLE_JAUM_G && c <= COMPATIBLE_JAUM_H) return MS949ToUniJong.get((int) c);
		else if (c >= UNICODE_JONG_G && c <= UNICODE_JONG_H) return (int) c - UNICODE_JONG_G;
		else return -1;
	}
	
	@SuppressWarnings("unused")
	private static boolean isChosung(char c) {
		if (c >= COMPATIBLE_JAUM_G && c <= COMPATIBLE_JAUM_H) return true;
		else if (c >= UNICODE_CHO_G && c <= UNICODE_CHO_H) return true;
		else return false;
	}
	
	@SuppressWarnings("unused")
	private static boolean isJungsung(char c) {
		if (c >= COMPATIBLE_MOUM_A && c <= COMPATIBLE_MOUM_E) return true;
		else if (c >= UNICODE_JUNG_A && c <= UNICODE_JUNG_E) return true;
		else return false;
	}
	
	private static boolean isJongsung(char c) {
		if (c >= COMPATIBLE_JAUM_G && c <= COMPATIBLE_JAUM_H) return true;
		else if (c >= UNICODE_JONG_G && c <= UNICODE_JONG_H) return true;
		else return false;
	}

	@Override
	public String convertEuckrToUTF8(byte[] euckrStream, CharsetType charType, boolean decodeNCR) {
		if (euckrStream == null || euckrStream.length == 0) return "";
		if (charType == null) return null;
		
		try {
			String str = new String(euckrStream, charType.toName());
			String result = euckrUnicodeToNormalUnicode(str);
			if (decodeNCR) result = ncrUnicodeToNonNCRUnicode(result);
			return result;
		} catch (UnsupportedEncodingException e) {
//			e.printStackTrace();
			return null;
		}
	}

	@Override
	public byte[] convertUTF8ToEuckr(String unicodeStr, CharsetType charType, EncodeMethod encodeMethod) {
		if (unicodeStr == null || unicodeStr.length() == 0) return new byte[0];
		if (charType == null) return unicodeStr.getBytes();
		
		try {
			switch (encodeMethod) {
			case ALL_NCR:	return toNCRString(unicodeStr, charType).getBytes(charType.toName());
			case NCR_EUCKR: return toEUCKRNCRString(unicodeStr, charType).getBytes(charType.toName());
			case EUCKR:		return normalUnicodeToEuckrUnicode(unicodeStr, charType).getBytes(charType.toName());
			default:		return unicodeStr.getBytes(charType.toName());
			}
		} catch (Exception e) {
//			e.printStackTrace();
			return null;
		}
	}
	
	private static String toNCRString(String unicodeStr, CharsetType charType) {
		StringBuilder ncr = new StringBuilder();
		String euckrStr = changeCharset(unicodeStr, charType);
		
		char[] krCa= euckrStr.toCharArray();
		char[] uniCa = unicodeStr.toCharArray();
		for (int i=0; i<uniCa.length; ++i) {
			int uniDecCode = (int) uniCa[i];
			// 깨진 문자(?)이고 원래 문자가 물음표(?)가 아니면 HTML NCR로 변환한다.
			if (isBrokenChar(krCa[i], uniCa[i])) {
				ncr.append(toNCR(uniDecCode));
			} else {
				ncr.append(String.valueOf(uniCa[i]));
			}
		}
		return ncr.toString();
	}
	
	private static String toEUCKRNCRString(String unicodeStr, CharsetType charType) {
		StringBuilder sb = new StringBuilder();
		String euckrStr = changeCharset(unicodeStr, charType);
		
		char[] krCa= euckrStr.toCharArray();
		char[] uniCa = unicodeStr.toCharArray();
		for (int i=0; i<uniCa.length; ++i) {
			int uniDecCode = (int) uniCa[i];
			// 깨진 문자(?)이고 원래 문자가 물음표(?)가 아니면 HTML NCR로 변환한다.
			if (isBrokenChar(krCa[i], uniCa[i])) {
				if (uniDecCode < UNICODE_HANGUL_GA || uniDecCode > UNICODE_HANGUL_HI) {
					sb.append(toNCR(uniDecCode));
				} else {
					char[] chars = toCompatibleJamo(uniCa[i]);
					for (char c : chars) {
						sb.append(c);
					}
				}
			} else {
				sb.append(String.valueOf(uniCa[i]));
			}
		}
		return sb.toString();
	}

	@Override
	public String convertEuckrToUTF8(String unicodeStr, CharsetType charType, boolean decodeNCR) {
		if (unicodeStr == null || unicodeStr.length() == 0) return "";
		if (charType == null) return null;
		String result = euckrUnicodeToNormalUnicode(unicodeStr);
		if (decodeNCR) result = ncrUnicodeToNonNCRUnicode(result);
		return result;
	}

	@Override
	public String convertUTF8ToEuckr2(String unicodeStr, CharsetType charType, EncodeMethod encodeMethod) {
		if (unicodeStr == null || unicodeStr.length() == 0) return "";
		if (charType == null) return unicodeStr;
		
		try {
			switch (encodeMethod) {
			case ALL_NCR:	return toNCRString(unicodeStr, charType);
			case NCR_EUCKR: return toEUCKRNCRString(unicodeStr, charType);
			case EUCKR:		return normalUnicodeToEuckrUnicode(unicodeStr, charType);
			default:		return unicodeStr;
			}
		} catch (Exception e) {
//			e.printStackTrace();
			return null;
		}
	}
	

}
